/*
 * ﻿Copyright (C) 2013-2014 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.eatestutil.ejbtestutil;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.SQLIntegrityConstraintViolationException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.EntityNotFoundException;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.OptimisticLockException;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Aspect to delete residue test entities (what have been created in each test method annotated with 
 * JUnit's <code>&#x40;Test</code> annotation) at end of each test method.<br />
 * <ul>
 *  <li>Only entity defined with <code>&#x40;Entity</code> annotation but with XML mapping definition 
 *  	will be subject of automatically removing from datastore.<br />  
 *  </li>
 *  <li>Only entity instance being instantiated from test method under the same single thread can be 
 *  	subject of automatic removal from datastore. <br />
 * 		In other words, <code>Entity</code> instance being instantiated in other (spawned) threads during 
 * 		test (such as some testing for locking mechanism) cannot be removed.<br /> 
 *  </li>
 *  <li>Test class being cross-cut must implement the <code>{@link EntityManagerFacadeProvider}</code> 
 * 		interface, in order to provide the access means to <code>EntityManager</code> instance under 
 * 		EJB context for appropriate persistence unit.<br />
 *  </li>
 *  <li>This may attempt removal of entity after finding it from datasource via 
 *  	<code>{@link EntityManagerFacadeProvider}</code> instance by either extracting ID value from 
 *  	entity object or constructing a ID object for <code>&#x40;IdClass</code> case if entity's version 
 *  	is updated in other thread, entity object is detached after grabbed by this, or some other cases.<br />
 *  </li>
 * </ul>
 * 
 * Note: please be advised to be acknowledging <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=166647"
 * >not-intuitive requirement in writing aop.xml for cflow or cflowbelow usage with LTW</a> because 
 * the default value to the <code>&#x40;Pointcut</code> annotation on the 
 * <code>{@link #pointcutCflowFromExecutionOfTestMethod()}</code> pointcut method uses cflow designator and 
 * the one on <code>{@link #pointcutCflowFromExecutionOfTestMethod()}</code> pointcut method uses 
 * cflowbelow designator.
 * 
 * @author Arata Y.
 */
@Aspect
public abstract class AbstractCleanupTestEntityAspect {
	Logger logger = LoggerFactory.getLogger( this.getClass());
		Logger getLogger() {
			return logger;
		}
	
	public static final String ExecutionOfTestMethodPointcutStr 
	= "execution( @org.junit.Test * *( ..))";
	/**
	 * <code>Pointcut</code> for test execution. <br />
	 * Default to <code>{@value #ExecutionOfTestMethodPointcutStr}</code> pointcut expression. Override 
	 * this with <code>&#x40;Pointcut</code> annotation as necessary.
	 */
	@Pointcut( value = AbstractCleanupTestEntityAspect.ExecutionOfTestMethodPointcutStr)
	public void pointcutAtExecutionOfTestMethod() {}
	
	/**
	 * <code>Pointcut</code> to cross-cut code flow from the <code>Pointcut</code> 
	 * specified with <code>{@link #pointcutAtExecutionOfTestMethod()}</code> method.<br />
	 */
	@Pointcut( value = "cflow( pointcutAtExecutionOfTestMethod())")
	public void pointcutCflowFromExecutionOfTestMethod() {}
	
	public static final String PersistingEntityPointcutStr 
	= "call( public * javax.persistence.EntityManager.persist( Object)) " 
		+ "|| call( public * javax.persistence.EntityManager.merge( ..))";
	/**
	 * <code>Pointcut</code> for initialization of <code>Entity</code> instance.<br /> 
	 * Default to <code>{@value #PersistingEntityPointcutStr}</code> pointcut expression. Override 
	 * this with <code>&#x40;Pointcut</code> annotation as necessary.
	 */
	@Pointcut( value = AbstractCleanupTestEntityAspect.PersistingEntityPointcutStr)
	public void pointcutAtPersistingEntity() {}

	public static class EntityRemovalPack {
		private EntityManagerFacade entityManagerFacade;
			public EntityManagerFacade getEntityManagerFacade() {
				return entityManagerFacade;
			}
			public void setEntityManagerFacade( final EntityManagerFacade entityManagerFacade) {
				this.entityManagerFacade = entityManagerFacade;
			}
		private List<Object> entityList = new LinkedList<Object>();
			public List<Object> getEntityList() {
				return entityList;
			}
			public void setEntityList( final List<Object> entityList) {
				this.entityList = entityList;
			}
	}
	static ThreadLocal<EntityRemovalPack> entityRemovalPackThreadLocal 
	= new ThreadLocal<EntityRemovalPack>();
		static ThreadLocal<EntityRemovalPack> getEntityRemovalPackThreadLocal() {
			return entityRemovalPackThreadLocal;
		}
		
	/**
	 * <code>AfterReturning</code> advise method to register <code>Entity</code> instance for 
	 * auto deletion later at each test exit.<br />
	 * The pointcut being used is the combination of the pointcuts specified by 
	 * <code>{@link #pointcutCflowFromExecutionOfTestMethod()}</code> method and 
	 * <code>{@link #pointcutAtPersistingEntity()}</code> method.
	 * @param joinPoint
	 */
	@AfterReturning( 
			pointcut = "pointcutCflowFromExecutionOfTestMethod() && pointcutAtPersistingEntity()",
			returning = "output")
	public void afterReturningFromPersistingEntity( final JoinPoint joinPoint, final Object output) {
		final Logger logger = getLogger();

		Object entityObj = output;
			if ( output == null) {
				entityObj = joinPoint.getArgs()[ 0];
				if ( entityObj == null) {
					return;
				}
			}
			if ( !entityObj.getClass().isAnnotationPresent( Entity.class)) {
				 /* Handle only entity object of what class has @Entity annotation but entity object via 
				  * XML mapping. This is due to necessity of means to identify id value of entity in order 
				  * to find entity with id value and remove it from datastore after failure of attempt of 
				  * directly removing entity first time. 
				  */
				throw new RuntimeException(
						String.format(
								"Could not find @%1$s annotation on %2$s class of %3$s entity.",
								Entity.class.getSimpleName(),
								entityObj.getClass().getSimpleName(),
								entityObj)
						);
			}
			
		final EntityRemovalPack entityRemovalPack 
		= AbstractCleanupTestEntityAspect.getEntityRemovalPackThreadLocal().get();
			if ( entityRemovalPack == null) {
				if ( logger.isErrorEnabled()) {
					logger.error(
							String.format(
									"Encountered unexpected state that, at returning from \"%1$s\" " 
									+ "join-point for persisting %2$s entity to datastore, %3$s instance " 
									+ "to hold %2$s entity has not been prepared for current thread. " 
									+ "Thereby, %2$s entity is skipped from automatic removal out of " 
									+ "datastore.",
									joinPoint.getSignature().toString(),
									entityObj,
									EntityRemovalPack.class.getSimpleName())
							);
				}
				return;
			}
			
		final Set<Object> entitySet = new LinkedHashSet<Object>( entityRemovalPack.getEntityList());
		final boolean addResult = entitySet.add( entityObj);
			if ( addResult) {
				final List<Object> entityList = new LinkedList<Object>( entitySet);
				entityRemovalPack.setEntityList( entityList);
			}
		
			if ( logger.isDebugEnabled()) {
				if ( addResult) {
					logger.debug( 
							String.format( 
									"%1$s entity has been acknowledged for auto removal from datasource as " 
									+ "a test residue at the end of test.",
									joinPoint.getTarget())
							);
				}
				else {
					logger.debug(
							String.format(
									"%1$s eneity has already been acknowledged for auto removal from " 
									+ "datasource as a test residue at the end of test before.",
									joinPoint.getTarget())
							);
				}
			}
	}
	
	/**
	 * <code>Pointcut</code> to cross-cut code flow below from the <code>Pointcut</code> 
	 * specified with <code>{@link #pointcutAtExecutionOfTestMethod()}</code> method.<br />
	 */
	@Pointcut( value="cflowbelow( pointcutAtExecutionOfTestMethod())")
	public static void pointcutOnSubsequentialExecutionOfJUnitTestMethod() {}
	
	/**
	 * <code>Before</code> advise to initialize <code>{@link EntityManagerFacadeProvider}</code> instance 
	 * (what is held in <code>ThreadLocal</code>) at each test entrance.<br />
	 * 
	 * @param joinPoint
	 */
	@Before( value 
			= "pointcutAtExecutionOfTestMethod() " 
			+ "&& !pointcutOnSubsequentialExecutionOfJUnitTestMethod()"
			+ "&& !cflow( adviceexecution())")
	public void beforeExecutionOfTestMethod( final JoinPoint joinPoint) {
		if ( !EntityManagerFacadeProvider.class.isInstance( joinPoint.getTarget())) return;
		
		final ThreadLocal<EntityRemovalPack> entityRemovalPackThreadLocal 
		= AbstractCleanupTestEntityAspect.getEntityRemovalPackThreadLocal();
			final EntityManagerFacadeProvider entityManagerFacadeProvider 
			= (EntityManagerFacadeProvider)joinPoint.getTarget();
			final EntityRemovalPack entityRemovalPack = new EntityRemovalPack();
				entityRemovalPack.setEntityManagerFacade( 
						entityManagerFacadeProvider.getEntityManagerFacade());
			entityRemovalPackThreadLocal.set( entityRemovalPack);
	}
	
	public static class EntityIdUnit {
		private Class<?> idClass = null;
			public Class<?> getIdClass() {
				return idClass;
			}
			public void setIdClass( final Class<?> idClass) {
				this.idClass = idClass;
			}
		private List<Field> idFields = new LinkedList<Field>();
			public List<Field> getIdFields() {
				return idFields;
			}
			public void clearIdFields() {
				getIdFields().clear();
			}
			public boolean addIdField( final Field idField) {
				return getIdFields().add( idField);
			}
			public boolean removeIdField( final Field idField) {
				return getIdFields().remove( idField);
			}
			public boolean containsIdField( final Field idField) {
				return getIdFields().contains( idField);
			}
		private List<Method> idMethods = new LinkedList<Method>();
			public List<Method> getIdMethods() {
				return idMethods;
			}
			public void clearIdMethods() {
				getIdMethods().clear();
			}
			public boolean addIdMethod( final Method idMethod) {
				return getIdMethods().add( idMethod);
			}
			public boolean removeIdMethod( final Method idMethod) {
				return getIdMethods().remove( idMethod);
			}
			public boolean containsIdMethod( final Method idMethod) {
				return getIdMethods().contains( idMethod);
			}
	}
	protected EntityIdUnit getEntityIdUnit( final Class<?> entityClass) {
		EntityIdUnit entityIdUnit = new EntityIdUnit();
			final Logger logger = getLogger();
			for( 	Class<?> entityClassObj = entityClass; 
					entityClassObj != null;
					entityClassObj = entityClassObj.getSuperclass()) {
				
				if ( 	!IdClass.class.isInstance( entityIdUnit.getIdClass()) 
						&& entityClassObj.isAnnotationPresent( IdClass.class)) {
					entityIdUnit.setIdClass( 
							entityClassObj.getAnnotation( IdClass.class).value());
				}
				for( Field field : entityClassObj.getDeclaredFields()) {
					if ( 	field.isAnnotationPresent( Id.class) 
							|| field.isAnnotationPresent( EmbeddedId.class)) {
						field.setAccessible( true);
						entityIdUnit.addIdField( field);
					}
				}
				for( Method method : entityClassObj.getDeclaredMethods()) {
					if ( 	method.isAnnotationPresent( Id.class)
							|| method.isAnnotationPresent( EmbeddedId.class)) {
						if ( method.getParameterTypes().length > 0) {
							if ( logger.isDebugEnabled()) {
								logger.debug(
										MessageFormat.format(
												"In searching reflection means to obtain ID value of {0} " 
												+ "entity class instance, skipped \"{1}\" method, despite " 
												+ "what is annotated as returning ID value, because it " 
												+ "requires argument rather than no argument.", 
												entityClass.getSimpleName(), 
												method.toString())
										);
							}
							continue;
						}
						
						method.setAccessible( true);
						entityIdUnit.addIdMethod( method);
					}
				}
			}
			
		return entityIdUnit;
	}
	
	protected List<Constructor<?>> getConstructorList( final Class<?> classObj) {
		ArrayList<Constructor<?>> constructors = null;
			for( Class<?> aClass = classObj; aClass != null; aClass = aClass.getSuperclass()) {
				constructors 
				= new ArrayList<Constructor<?>>( 
						Arrays.asList( aClass.getDeclaredConstructors()));
				if ( constructors.size() > 0) {
					for( Constructor<?> constructor : constructors) {
						constructor.setAccessible( true);
					}
					break; //for
				}
			}
		return constructors;
	}
	
	protected Object getEntityIdValue( final Object entity) {
		final EntityIdUnit entityIdUnit = getEntityIdUnit( entity.getClass());
		final Class<?> idClass = entityIdUnit.getIdClass();
		final List<Field> idFields= entityIdUnit.getIdFields();
		final List<Method> idMethods = entityIdUnit.getIdMethods();

		if ( (idFields.size() + idMethods.size()) < 1) {
			throw new IllegalArgumentException(
					MessageFormat.format(
							"Failed to obtain ID value of {0} bean instance via reflection since " 
							+ "could not find access means to its ID value via reflection.",
							entity.toString())
					);
		}
			
		if ( idClass == null) {
			if ( (idFields.size() + idMethods.size()) > 1) {
				throw new IllegalArgumentException(
						MessageFormat.format(
								"Failed to obtain ID value of {0} bean instance via reflection since " 
								+ "detected multiple fields/methods to construct ID value despite " 
								+ "{1} class was not annotated with @{2} annotatiion.",
								entity.toString(),
								entity.getClass().getSimpleName(),
								IdClass.class.getSimpleName())
						);
			}
			
			if ( idFields.size() > 0) {
				try {
					return idFields.get( 0).get( entity);
				}
				catch( Exception exception) {
					throw new RuntimeException( 
							MessageFormat.format(
									"Failed to obtain ID value of {0} bean instance via " 
									+ "reflection.",
									entity.toString()),
							exception);
				}
			}
			else {
				try {
					return idMethods.get( 0).invoke( entity, new Object[]{});
				}
				catch( Exception exception) {
					throw new RuntimeException( 
							MessageFormat.format(
									"Failed to obtain ID value of {0} bean instance via " 
									+ "reflection.",
									entity.toString()),
							exception);
				}
			}
		}
		else { // construct composite primary key object of idClass type
			List<Constructor<?>> constructors = getConstructorList( idClass);
				if ( constructors == null) {
					throw new RuntimeException(
							MessageFormat.format(
									"Error: could not get constructor of {0} ID class in order to " 
									+ "instantiate a composite primary key object of {1} entity by " 
									+ "reflection.",
									idClass.getSimpleName(), 
									entity)
							);
				}
			// Get constructor with no argument of idClass type
			Constructor<?> noArgConstructor = null;
				for( Constructor<?> constructor : constructors) {
					if ( constructor.getParameterTypes().length > 0) continue;
					if ( Modifier.isPrivate( constructor.getModifiers())) {
						if ( constructors.size() > 1) {
							final Logger logger = getLogger();
								if ( logger.isDebugEnabled()) {
									logger.debug(
											MessageFormat.format(
													"Found out that, in constructing composite primary " 
													+ "key object of {0} entity, no-argument constructor of " 
													+ "{1} id class has private visibility defined.",
													entity,
													idClass.getSimpleName())
											);
								}
							break;
						}
						else {
							throw new RuntimeException(
									MessageFormat.format(
											"Error: Found only private constructor in constructing " 
											+ "composite primary key object of {0} entity by relection.",
											entity)
									);
						}
					}
					noArgConstructor = constructor;
					break;
				}
			
			// Instantiate a composite primary key object
			Object idObj = null;
				if ( noArgConstructor != null) {
					try {
						idObj = noArgConstructor.newInstance( new Object[]{});
					}
					catch( Exception exception) {
						throw new RuntimeException(
								MessageFormat.format(
										"Failure in instatiating a composite primary key object of {0} " 
										+ "entity via \"{1}\" constructor by reflection.",
										entity, 
										noArgConstructor), 
								exception);
					}
				}
				else {
					final Logger logger = getLogger();
						if ( logger.isDebugEnabled()) {
							logger.debug(
									MessageFormat.format(
											"Found out that {0} class does not have no-argument " 
											+ "constructor defined. ",
											idClass.getClass().getSimpleName())
									);
						}
					// Attempt to pick up constructor of what arguments are matching with field/method 
					// with @ID annotation on class of entity.
//TODO Better to change to use constructor of idClass with the less number of arguments than the number of field/method with @ID annotation on class of entity.
					Constructor<?> constructor = null;
					final List<Object> constructorParameterTypes = new LinkedList<Object>();
						for( Constructor<?> constructorObj : constructors) {
							List<Field> idFieldsCopy = new LinkedList<Field>( idFields);
							Iterator<Field> idFieldsIterator = idFieldsCopy.iterator();
							List<Method> idMethodsCopy = new LinkedList<Method>( idMethods);
							Iterator<Method> idMethodsIterator = idMethodsCopy.iterator();
							for( 	int index = 0; 
									index < constructorObj.getParameterTypes().length;
									index++) {
								
								while( idFieldsIterator.hasNext()) {
									Field idField = idFieldsIterator.next();
									if ( 	idField.getType().isAssignableFrom( 
												constructorObj.getParameterTypes()[ index]) 
											|| constructorObj.getParameterTypes()[ index].isAssignableFrom( 
													idField.getType())) {
										constructorParameterTypes.add( idField);
										idFieldsIterator.remove();
										break;
									}
								}
								
								while( idMethodsIterator.hasNext()) {
									Method idMethod = idMethodsIterator.next();
									if ( 	idMethod.getReturnType().isAssignableFrom( 
												constructorObj.getParameterTypes()[ index])
											|| constructorObj.getParameterTypes()[ index].isAssignableFrom( 
													idMethod.getReturnType())) {
										constructorParameterTypes.add( idMethod);
										idMethodsIterator.remove();
										break;
									}
								}
							}
							if ( constructorParameterTypes.size() 
									== constructorObj.getParameterTypes().length) {
								constructor = constructorObj;
								break;
							}
							
							constructorParameterTypes.clear();
						}
					if ( constructor == null) {
						throw new RuntimeException(
								MessageFormat.format(
										"Failed to identify appropriate constructor to instantiate a " 
										+ "composite primary key object of {0} id class for {1} entity.",
										idClass.getSimpleName(),
										entity)
								);
					}
					
					final List<Object> constructorParameters = new LinkedList<Object>();
						try {
							for( Object parameterType : constructorParameterTypes) {
								if ( Field.class.isInstance( parameterType)) {
									constructorParameters.add( ((Field)parameterType).get( entity));
								}
								else {
									constructorParameters.add(
											((Method)parameterType).invoke( entity, new Object[]{}));
								}
							}
						}
						catch( Exception exception) {
							throw new RuntimeException(
									MessageFormat.format(
											"Failure in preparing parameters to \"{0}\" constructor for " 
											+ "instantiating an object of {1} id class, in order to use " 
											+ "it for finding {2} entity in datasource via EntityManager.",
											constructor, 
											idClass.getSimpleName(), 
											entity),
									exception);
						}
					
					try {
						idObj = constructor.newInstance( constructorParameters.toArray());
					}
					catch( Exception exception) {
						throw new RuntimeException(
								MessageFormat.format(
										"Failure in instatiating a composite primary key object of {0} " 
										+ "entity via \"{1}\" constructor with feeding next arguments " 
										+ "{2} by reflection.",
										entity, 
										constructor, 
										constructorParameters.toString()), 
								exception);
					}
				}
	
			// For each ID field of class of entity, identify corresponding field in idClass  
			final List<Field> idClassIdFields = new LinkedList<Field>();
				for( Field idField : idFields) {
					String idFieldName = idField.getName();
					Class<?> idFieldClass = idField.getType();
					boolean isMatchFound = false;
					for( 	Class<?> classObj = idClass; 
							classObj != null; 
							classObj = classObj.getSuperclass()) {
						for( Field field : classObj.getDeclaredFields()) {
							if ( idFieldName.equals( field.getName())) {
								if ( 	!idFieldClass.isAssignableFrom( field.getType()) 
										&& !field.getType().isAssignableFrom( idFieldClass)) {
									throw new UnsupportedOperationException(
											MessageFormat.format(
													"In searching corresponding field in {0} id class for " 
													+ "{1} id field of {2} entity class, encountered " 
													+ "situation that those types do not match despite " 
													+ "those name match; indicating possibility of join " 
													+ "column. {3} does not support the case that the " 
													+ "entity ({4}) (what was picked up by pointcut) is " 
													+ "not instance managed by EJB container and uses join " 
													+ "column as key.",
													idClass.getSimpleName(), 
													idField, 
													entity.getClass().getSimpleName(),
													this.getClass().getSimpleName(), 
													entity)
											);
								}
								idClassIdFields.add( field);
								isMatchFound = true;
							}
						}
						if ( isMatchFound) break;
					}
//TODO Check on JPA spec whether ID field of entity class must correspond to a field but a method in IdClass (and vise vasa) 
					if ( !isMatchFound) {
						throw new RuntimeException(
								MessageFormat.format(
										"Could not locate corresponding field in {0} id class for {1} id " 
										+ "field of {2} entity class in constructing a composite primary " 
										+ "key object of {3} entity by relection. ",
										idClass.getSimpleName(), 
										idField, 
										entity.getClass().getSimpleName(),
										entity)
								);
					}
				}
			
			final List<Method> idClassIdMethods = new LinkedList<Method>();
				for( Method idMethod : idMethods) {
					String idMethodName = idMethod.getName();
					String idSetterMethodName = idMethodName;
						if ( idMethodName.startsWith( "get")) {
							idSetterMethodName = idMethodName.substring( 3);
						}
						idSetterMethodName = "set".concat( idSetterMethodName);
					Class<?> idMethodClass = idMethod.getReturnType();
					boolean isMatchFound = false;
					for( 	Class<?> classObj = idClass; 
							classObj != null; 
							classObj = classObj.getSuperclass()) {
						for( Method method : classObj.getDeclaredMethods()) {
							if ( idMethodName.equals( method)) {
								Class<?>[] parameterTypes = method.getParameterTypes();
								if ( parameterTypes.length != 1) {
									if ( logger.isDebugEnabled()) {
										logger.debug(
												MessageFormat.format(
														"In searching corresponding setter method in {0} " 
														+ "id class for \"{1}\" id getter method of {2} " 
														+ "entity class, skipped \"{3}\" setter method " 
														+ "because it requires more than single argument. " 
														+ "{4} does not support such case. The code flow " 
														+ "reached here as either {6} entity was " 
														+ "instantiated outside of EJB container, entity's " 
														+ "version is updated, or entity object is detached " 
														+ "after grabbed by this; in order to work around, " 
														+ "please either override pointcut definition or " 
														+ "make change on code flow mentioned before.",
														idClass.getSimpleName(), 
														idMethod, 
														entity.getClass().getSimpleName(),
														method, 
														this.getClass().getSimpleName(), 
														entity)
												);
									}
									continue;
								}
								if ( 	!idMethodClass.isAssignableFrom( method.getParameterTypes()[0])
										&& !method.getParameterTypes()[0].isAssignableFrom( idMethodClass)) {
									if ( logger.isDebugEnabled()) {
										logger.debug(
												MessageFormat.format(
														"In searching corresponding setter method in {0} " 
														+ "id class for \"{1}\" id getter method of {2} " 
														+ "entity class, skipped \"{3}\" setter method " 
														+ "because type of its argument is {4} but {5}.",
														idClass.getSimpleName(), 
														idMethod, 
														entity.getClass().getSimpleName(),
														method,
														method.getParameterTypes()[0].getSimpleName(), 
														idMethodClass.getSimpleName())
												);
									}
									continue;
								}
								
								idClassIdMethods.add( method);
								isMatchFound = true;
							}
						}
						
						if ( isMatchFound) break;
					}
					if ( !isMatchFound) {
						throw new RuntimeException(
								MessageFormat.format(
										"Could not locate corresponding setter method in {0} id class for " 
										+ "\"{1}\" id getter method of {2} entity class in constructing a " 
										+ "composite primary key object of {3} entity by relection. ",
										idClass.getSimpleName(), 
										idMethod, 
										entity.getClass().getSimpleName(),
										entity)
								);
					}
				}
				
			// Attempt to set value to each ID field in idObj (composite primary key object)
			try {
				for( int index = 0; index < idFields.size(); index++) {
					Object key = idFields.get( index).get( entity);
					
					idClassIdFields.get( index).set( idObj, key);
				}
				for( int index = 0; index < idMethods.size(); index++) {
					Object key = idMethods.get( index).invoke( entity, new Object[]{});
					
					idClassIdMethods.get( index).invoke( idObj, key);
				}
			}
			catch( Exception exception) {
				throw new RuntimeException(
						MessageFormat.format(
								"Failure in getting each id value from {0} entity and setting it to {1} " 
								+ "key object in order to find {0} entity (what is managed by EJB " 
								+ "container and is most updated) from datasource via EntityManager's " 
								+ "find method.",
								entity, 
								idObj),
						exception);
			}
				
			return idObj;
		}
	}

	/**
	 * Remove specified entity via given <code>EntityManagerFacade</code> instance.<br />
	 * This will flush <code>EntityManager</code> in order to intentionally catch exception 
	 * before timing of regular commit.<br />
	 * This should be called under code flow from 
	 * <code>{@link EntityManagerFacade#runExternalMethod(Object, Method, Object...)}</code> method.<br /> 
	 * 
	 * @param entity for removal out of datastore.
	 * @param entityManagerFacade to provide access means to <code>EntityManager</code> under EJB context.
	 * @return null when succeed for entity removal. Otherwise exception object caught during removal 
	 * will be returned. 
	 */
	protected Exception removeEntitiy( 
			final Object entity, final EntityManagerFacade entityManagerFacade) {
		try {
			if ( entityManagerFacade.contains( entity)) {
				entityManagerFacade.refresh( entity);
				entityManagerFacade.remove( entity);
			}
			else {
				entityManagerFacade.remove( 
						entityManagerFacade.merge( entity));
			}
			
			entityManagerFacade.flush();
				// flush EntityManager to intentionally catch exception if any occurs.
				// This may not be work under Spring framework.
		}
		catch( Exception exception) {
			return exception;
		}
		
		return null; 
	}
	
		private Method removeEntitiyMethod;
			protected void setRemoveEntitiyMethod(Method removeEntitiyMethod) {
				this.removeEntitiyMethod = removeEntitiyMethod;
			}
			public Method getRemoveEntitiyMethod() {
				if ( removeEntitiyMethod == null) {
					final String methodName = "removeEntitiy";
					Method method = null;
						for( 
								Class<?> classObj = this.getClass(); 
								classObj != null; 
								classObj = classObj.getSuperclass()) {
							try {
								method
								= classObj.getDeclaredMethod( 
										methodName, 
										new Class<?>[]{ Object.class, EntityManagerFacade.class});
								break;
							} 
							catch ( NoSuchMethodException exception) { // do nothing
							}
						}
					if ( method == null) {
						throw new RuntimeException(
								MessageFormat.format(
										"Failed to locate {0} method in {1} class.",
										methodName, 
										this.getClass().getSimpleName())
								);
					}
					setRemoveEntitiyMethod( method);
				}
				return removeEntitiyMethod;
			}

	protected void removeEntitiyViaId( 
			final Object entity, final Object key, final EntityManagerFacade entityManagerFacade) {
		
		Object dsEntity = entityManagerFacade.getReference( entity.getClass(), key);
		entityManagerFacade.remove( dsEntity);
		
		final Logger logger = getLogger();
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Removed %1$S entity (as substitution of %2$s) from " 
								+ "datasource as a test residue.",
								dsEntity, 
								entity)
						);
			}
	}
	
		private Method removeEntitiyViaIdMethod;
			protected void setRemoveEntitiyViaIdMethod( final Method removeEntitiyViaIdMethod) {
				this.removeEntitiyViaIdMethod = removeEntitiyViaIdMethod;
			}
			public Method getRemoveEntitiyViaIdMethod() {
				if ( removeEntitiyViaIdMethod == null) {
					final String methodName = "removeEntitiyViaId";
					Method method = null;
						for( 	Class<?> classObj = this.getClass(); 
								classObj != null; 
								classObj = classObj.getSuperclass()) {
							try {
								method
								= classObj.getDeclaredMethod( 
										methodName, 
										new Class<?>[]{ Object.class, Object.class, EntityManagerFacade.class});
								break;
							} 
							catch ( NoSuchMethodException exception) { // do nothing
							}
						}
					if ( method == null) {
						throw new RuntimeException(
								MessageFormat.format(
										"Failed to locate {0} method in {1} class.",
										methodName, 
										this.getClass().getSimpleName())
								);
					}
					setRemoveEntitiyViaIdMethod( method);
				}
				return removeEntitiyViaIdMethod;
			}

	protected List<Object> removeEntityBunch( 
			final List<Object> entities, final EntityManagerFacade entityManagerFacade) {
		final List<Object> binedEntityList = new LinkedList<Object>();
		
		final Logger logger = getLogger();
		
		final int totalEntityCount = entities.size();
		
		for( Object entity : entities.toArray()) {
			// Attempt direct removal of entity first  
			Exception exceptionObj = null;
				try {
					exceptionObj = (Exception)entityManagerFacade.runExternalMethod( 
							this, getRemoveEntitiyMethod(), entity, entityManagerFacade);
				} 
				catch( IllegalAccessException | InvocationTargetException exception) {
					Throwable cause = exception.getCause();
						if ( RuntimeException.class.isInstance( cause)) {
							throw (RuntimeException)cause;
						}
					throw new RuntimeException(
							String.format(
									"Failure in removing %1$s entity from datastore.",
									entity),
							exception);
				}
			if ( exceptionObj == null) {
				if ( logger.isInfoEnabled()) {
					logger.info( 
							String.format(
									"Removed %1$s entity from datasource as a test residue.",
									entity.toString())
							);
				}
				continue; 
			}
			
			// entity was not removed somehow; possibly either OptimisticLockException by using old 
			// version of entity or SQLIntegrityConstraintViolationException by some live reference 
			// from other entity to that entity exists.
			// For SQLIntegrityConstraintViolationException case, once other entities are removed 
			// (hopefully reference is also eliminated), removal will be re-attempted.
			// For OptimisticLockException case, at below, removal attempt is going to be made after 
			// finding most-updated one with primary key ID value.
			Throwable reasonableCause = null;
				for( 	Throwable throwable = exceptionObj; 
						throwable != null; 
						throwable = throwable.getCause()) {
					if ( OptimisticLockException.class.isInstance( throwable)) {
						reasonableCause = throwable;
							if ( logger.isDebugEnabled()) {
								logger.debug(
										String.format(
												"Removal of %1$s will be re-attempted later by finding it " 
												+ "using its id value first since having encountered %2$s " 
												+ "at first direct removing attempt.",
												entity,
												OptimisticLockException.class.getSimpleName())
										);
							}
						break;
					}
					if ( SQLIntegrityConstraintViolationException.class.isInstance( throwable)) {
						if ( totalEntityCount > 1) {
							reasonableCause = throwable;
								if ( logger.isDebugEnabled()) {
									logger.debug(
											String.format(
													"Removal of %1$s will be re-attempted later once " 
													+ "removal of other entities are attempted since having " 
													+ "encountered %2$s at first direct removing attempt.",
													entity,
													SQLIntegrityConstraintViolationException.class
														.getSimpleName())
											);
								}
						}
						break;
					}
				}
			if ( reasonableCause == null) {
				if ( RuntimeException.class.isInstance( exceptionObj)) {
					throw (RuntimeException)exceptionObj;
				}
				throw new RuntimeException( 
						String.format(
								"Failure in removing %1$s entity from datastore.",
								entity),
						exceptionObj);
			}
			else if ( SQLIntegrityConstraintViolationException.class.isInstance( reasonableCause)) {
				// Add entity to binedEntityList for later re-attempt of removal after other entities 
				// are removed (hopefully reference is also eliminated).
				binedEntityList.add( entity);
				continue;
			}
			
			// Re-attempt removal of entity after finding most-updated one with primary key ID value
			Object key = getEntityIdValue( entity);
				if ( logger.isDebugEnabled()) {
					logger.debug( 
							MessageFormat.format(
									"Detected {0} as primary key value (ID) of {1} entity in order to " 
									+ "pull most updated {2} entity with it out of datastore and then " 
									+ "remove that entity from datastore.",
									key,
									entity,
									entity.getClass().getSimpleName()) 
							);
				}
			
			try {
				entityManagerFacade.runExternalMethod( 
						this, getRemoveEntitiyViaIdMethod(), entity, key, entityManagerFacade);
			}
			catch( Exception exception) {
				boolean isCauseAnticipated = false;
					for( 	Throwable throwable = exception; 
							throwable != null; 
							throwable = throwable.getCause()) {
						if ( EntityNotFoundException.class.isInstance( throwable)) {
							isCauseAnticipated = true;
							if ( logger.isInfoEnabled()) {
								logger.info( 
										String.format(
												"Skipped removing %1$s entity from datasource, since, in " 
												+ "identifing it with %2$s key object, that entity was not " 
												+ "found in datastore.", 
												entity,
												key)
										);
							}
							break;
						}
						if ( SQLIntegrityConstraintViolationException.class.isInstance( throwable)) {
							if ( totalEntityCount > 1) {
								binedEntityList.add( entity);
									// Add entity to joinedEntityList for later re-attempt of removal, 
									// since it might be caused by violation of foreign key constraint.
								isCauseAnticipated = true;
								
									if ( logger.isDebugEnabled()) {
										logger.debug(
												String.format(
														"Removal of %1$s will be re-attempted later once " 
														+ "removal of other entities are attempted since having " 
														+ "encountered %2$s at first direct removing attempt.",
														entity,
														SQLIntegrityConstraintViolationException.class
															.getSimpleName())
												);
									}
							}
							break;
						}
					}
				if ( !isCauseAnticipated) {
					throw new RuntimeException(
							MessageFormat.format(
									"Failure in attempting to remove {0} entity (what was pulled by using " 
									+ "{1} key object) as substitute of {2} entity, from datasource.",
									entity.getClass().getSimpleName(),
									key,
									entity),
							exception);
				}
			}
		}
		
		return binedEntityList;
	}
			
	/**
	 * <code>After</code> advise to remove <code>Entity</code> instance at each test exit.<br />
	 * The pointcut being used is the combination of the pointcut specified by 
	 * <code>{@link #pointcutAtExecutionOfTestMethod()}</code> method and the negation of 
	 * the pointcut given by <code>{@link #pointcutOnSubsequentialExecutionOfJUnitTestMethod()}</code> 
	 * method.
	 * 
	 * @param joinPoint
	 */
	@After( value 
			= "pointcutAtExecutionOfTestMethod() " 
			+ "&& !pointcutOnSubsequentialExecutionOfJUnitTestMethod()"
			+ "&& !cflow( adviceexecution())")
	public void afterExecutionOfTestMethod( final JoinPoint joinPoint) 
	throws IllegalAccessException, InvocationTargetException {
		final EntityRemovalPack entityRemovalPack 
		= AbstractCleanupTestEntityAspect.getEntityRemovalPackThreadLocal().get();
		if ( entityRemovalPack != null) {
			final EntityManagerFacade entityManagerFacade = entityRemovalPack.getEntityManagerFacade();
				if ( entityManagerFacade == null) {
					throw new IllegalStateException(
							String.format(
									"%1$s instance has not been set.",
									EntityManagerFacade.class.getSimpleName())
							);
				}
				
			final Logger logger = getLogger();
			final String pointcutSignatureStr 
			= ((CodeSignature)joinPoint.getSignature()).toShortString();
				
			List<Object> entityList = entityRemovalPack.getEntityList();
				if ( ( entityList.size() > 0) && logger.isInfoEnabled()) {
					logger.info( 
							MessageFormat.format(
									"Recognized {0,number} " 
									+ "{0,choice,-1<entity|1<entities} sent to datastore while " 
									+ "running of {1}, and going to remove those: {2}.",
									entityList.size(),
									pointcutSignatureStr,
									entityList.toString())
							);
				}
			
			final int totalEntityCount = entityList.size();
			entityList = removeEntityBunch( entityList, entityManagerFacade);
			if ( ( entityList.size() > 0) && (totalEntityCount > entityList.size())) {
				// re-attempt of removal of entity what could not be removed by violation of 
				// foreign key constraint
				if ( logger.isInfoEnabled()) {
					logger.info( 
							MessageFormat.format(
									"Going to re-attempt removing next {0,number} " 
									+ "{0,choice,-1<entity|1<entities} what might not have been able to " 
									+ "be removed due to possibly violation of foreign key constraint at " 
									+ "first time attempt: {1}.",
									entityList.size(),
									entityList.toString())
							);
				}
				
				entityList = removeEntityBunch( entityList, entityManagerFacade);
			}
			
			if ( entityList.size() > 0) {
//TODO implement means to provide option of whether just logging incident of remained entity or throw exception.
				if ( logger.isErrorEnabled()) {
					logger.error(
							MessageFormat.format(
									"Failed to remove next {0,number} {0,choice,1#entity|1<entities} " 
									+ "what was sent to datastore while running of {1}: {2}",
									entityList.size(),
									pointcutSignatureStr,
									entityList.toString())
							);
				}
			}
		}
		
		AbstractCleanupTestEntityAspect.getEntityRemovalPackThreadLocal().remove();
	}
}